/**
 * <code>jsMotif</code> is a template rendering class extended by {@link nokia.places.APITemplate}.
 * This method creates a new instance of <code>jsMotif</code>.
 * @class
 * 
 * @publish
 * @param {Object} params An initialization object with the following properties
 * <ul>
 *      <li>
 *          <code>template</code> - (mandatory) a value which represents a template it can be:
 *          <ul>
 *              <li>a {String} containing HTML elements, e.g '&lt;div>&lt;a>&lt;/a>&lt;/div>'</li>
 *              <li>a {String} containing an existing DOM element id (the template pattern is 
 *              extracted from the DOM node</li>
 *              <li>a {Object} reference to existing DOM node in javascript</li>
 *          </ul>
 *      </li>
 *      <li>
 *          <code>targetNode</code> - the element into which the template is to be rendered; its value 
 *          can be:
 *          <ul>
 *              <li>a {String} which represents an existing element id</li>
 *              <li>an {Object} which represents a reference to an existing DOM element in JavaScript</li>
 *          </ul>
 *      </li>
 *      <li>
 *          <code>functions</code> - an object/hashmap which contains a user-defined function that
 *          may be used to render or to affect rendering of the element in template.
 *      </li>
 *      <li>
 *          <code>event</code> - an array of objects which specify the events attached to template nodes and
 *          the event handlers; each object in the array has the following attributes: 
 *          <ul>
 *              <li>
 *                  <code>rel</code> - a {String} indicating the template node to which to attach the event
 *              </li>
 *              <li>
 *                  <code>name</code> - a {String} providing the name of the attached event; the supported
 *                  event names are 'keydown', 'keyup', 'keypress', 'click', 'dblclick', 'mousedown', 
 *                  'mousemove', 'mouseout', 'mouseover', 'mouseup', 'mouseenter', 'mouseleave', 'blur', 
 *                  'change', 'focus', 'reset', 'select', 'submit', 'abort'
 *              </li>
 *              <li>
 *                  <code>handler</code> - a caller-defined {Function} to be invoked when the user 
 *                  triggers the attached event; the function receives the data object as an argument and 
 *                  'this' within the function refers to the element on which the event was triggered
 *              </li>
 *          </ul>
 *      </li>
 *  </ul>
 *  
 *  @memberOf jsMotif
 */
jsMotif.Template = (function(){
    
    var $E = jsMotif.errors,
        _dom = jsMotif.dom,
        _selector = jsMotif.selector;
        
    var _stripBrackets = function (value) {
        if (value && '{' === value.charAt(0) &&  '}' === value.charAt(value.length-1)) {
            return value.substr(1).substr(0, value.length-2);
        }
        return false;
    };
    
    /**
     * Function returns a function path within json object
     * @param {Object} path
     * 
     * @private
     */
    var getFunctionPath = function (path){
        var functionMatch = path.match(/(\S+)\(\)/);
        if(functionMatch){
            return functionMatch[1];
        }
    };

    /**
     * Filling target node with template
     * @param {Object} node
     * @param {Object} jsonObject
     * @param {Object} fragment
     * 
     * @private
     */
    var fillNode = function(node, jsonObject, fragment){
        var cloned = fragment.cloneNode(true);

        this.renderObject(cloned, jsonObject);
        _dom.append(node, cloned);
    };
    
    /**
     * Funtion calls the predefined function for provided control path 
     * @param {Object} value
     * @param {Object} controlPath
     * @param {Object} jsonObject
     * @param {Object} node
     * 
     * @private
     */
    var getFunctionValueForUndef = function (value, controlPath, jsonObject, node) {
        if(typeof(value) === 'undefined'){
            var functionPath = getFunctionPath(controlPath);
            value = (functionPath && this.predefinedFunctions[functionPath])? this.predefinedFunctions[functionPath].call(node, jsonObject) : value;
        }
        return value;
    };

    /**
     * Function returns data using attribute value and json object
     * 
     * @param {Object} node
     * @param {Object} path
     * @param {Object} jsonObject
     */
    var getAttributeData = function (node, value, jsonObject, decode) {
        
        if(decode){
            value = decodeURI(value);
        }
        
        value = _stripBrackets(value);
        if (false === value) {
            return;
        }
        
        var data = _selector.resolveCondition.call(jsonObject, value);
        data = getFunctionValueForUndef.call(this, data, value, jsonObject, node);
        return data;
        
    };

    /**
     * Function wraps getAttributeData function and allows for multiple json paths,
     * in some attributes like fill, url etc
     * @param {Object} node
     * @param {Object} value
     * @param {Object} jsonObject
     * @param {Object} decode
     */
    var parseAttrubute = function(node, value, jsonObject, decode){
        var self = this,
            result = value.replace(/(\{[^\{\}]+\})/g, function($0, closuredJsonPath){
                var data = getAttributeData.call(self, node, closuredJsonPath, jsonObject, decode);
                return (data)? data : '';
            });
            
        return result;
    };

    


    /**
     * Handler for fill attribute
     * When renderObject() finds fill attribute in node, this function is called
     * Registered using registerAttr()
     *
     * @param {Object} node
     * @param {String} value of fill attribute
     * @param {Object} jsonObject 
     */
    var onAttrFill = function(node, value, jsonObject){
        var data = parseAttrubute.call(this, node, value, jsonObject);
        if (data) {
            if (data instanceof Array) {
                data = data.join(', ');
            }
            
            node.innerHTML = data;
            return data;
        }else{
            _dom.hide(node);
            return false;    
        }        
    };

    /**
     * Helper handler for onAttrHref and onAttrSrc
     * This function is called when renderObject() finds the attribute <code>src</code>
     * in a node.
     * Registered using registerAttr()
     *
     * @param {Object} node
     * @param {String} value of src attribute
     * @param {Object} jsonObject 
     */
    var onAttrUrl = function(node, value, jsonObject, attr){
        var data = parseAttrubute.call(this, node, value, jsonObject, true),
            nodeName = node.nodeName.toLowerCase();
        if (typeof(data) === 'string') {
            if(nodeName === 'a' || nodeName === 'link'){
                node.href = data;   
            }else{
                node.src = data;
            }
        }
        return data;
    };

    /**
     * Handler for attribute mailto inserts clicable link on node with email
     * 
     * @param {Object} node
     * @param {Object} value
     * @param {Object} jsonObject
     */
    var onAttrMailto = function(node, value, jsonObject){
        var data = parseAttrubute.call(this, node, value, jsonObject, true);
        if (data) {
            node.setAttribute('href', 'mailto:'+data);
        }
        return data;
    };
    
    /**
     * Handler for attribute if hides or shows a node, depending on whether 
     * the specified condition is true or false.
     * 
     * @param {Object} node
     * @param {Object} value
     * @param {Object} jsonObject
     */
    var onAttrIf = function (node, value, jsonObject) {
        var data = getAttributeData.call(this, node, value, jsonObject);
        if(!data){
            _dom.hide(node);
            return false;
        }
    };
    
    /**
     * Handler function for each attribute repeats inner html respectively for every element of the array
     * 
     * @param {Object} node
     * @param {Object} value
     * @param {Object} jsonObject
     */
    var onAttrEach = function (node, value, jsonObject){
        var data = getAttributeData.call(this, node, value, jsonObject);
        if (data instanceof Array && data.length !== 0) {
            var tempFrag = _dom.createFragment();
            _dom.moveChildNodes(tempFrag, node);
            for(var i=0, l=data.length; i<l; i++){
                data[i]._index = (i+1);
                fillNode.call(this, node, data[i], tempFrag);
            }
        }else{
            _dom.hide(node);
        }
        
        return false;
    };

    /**
     * Handler for tpl attribute. The attribute's value is the name of a template. This can be the 
     * id of the HTML that defines the template or the name of a standard template file provided with the 
     * API, for example, "nokia.general.place", "nokia.general.address", "nokia.general.contact", etc., where
     * the last element is the base name of an HTML file.

     * When renderObject() finds tpl attribute in node, this function is called
     * Registered using registerAttr()
     *
     * @param {Object} node
     * @param {String} value of tpl attribute
     */
    var onAttrTpl = function(node, value){
        var el = _dom.byId(value);
        if(el){
             _dom.moveChildNodes(node, el.cloneNode(true));
             node.removeAttribute('tpl');
        }
    };
    
    /**
     * Handler for rel attribute. The role of the attribute is primarily to assign events to 
     * HTML elements in a template, but also to identify HTML elements as targets for modules.
     * @param {Object} node
     * @param {Object} value
     * @param {Object} jsonObject
     */
    var onAttrRel = function (node, value, jsonObject) {
        var events = this.eventMap[value],
            handler = function(event, node, jsonObject){
                return function(e){
                    event.handler.call(node, jsonObject, e);
                };
            };
            
        if (events) {
            for (var i = 0, l = events.length; i < l; i++) {
                var event = events[i];
                if (event && event.name && typeof(event.handler) === 'function') {
                    _dom.addEvent(node, event.name, handler(event, node, jsonObject));
                }
            }
        }
    };
    
    /**
     * Function initializes the jsMotif object (creates template pattern)
     * @private
     */
    var initialize = function(){
        var tempElement = _dom.create('div',{});
        tempElement.innerHTML = this.templateBody;
        this.fragment = _dom.createFragment();
        _dom.moveChildNodes(this.fragment, tempElement);
    };
    
    var jsMotifClass = function (params) {
        this.targetElem = _dom.elem(params.targetNode) || _dom.elem(params.template);
        
        this.mapping = [];
        var templateHolder = _dom.elem(params.template);
        if(templateHolder){
            this.templateBody = templateHolder.innerHTML;    
        }else{
            this.templateBody = params.template;
        }

        this.debug = params.debug || 0;
        this.predefinedFunctions = params.functions || {};
        this.attributes = [];
        
        this.registerAttr('tpl', onAttrTpl);
        this.registerAttr('if', onAttrIf);
        this.registerAttr('each', onAttrEach);
        this.registerAttr('fill', onAttrFill);
        this.registerAttr('url', onAttrUrl);
        this.registerAttr('rel', onAttrRel);
        this.registerAttr('mailto', onAttrMailto);
        
        this.eventMap = {};
        this.addEventMap(params.events);
        
        initialize.call(this);
    };

    /**
     * Function collects the rel attributes and stores to 'this' context
     * since it works recursively for child nodes use it within result object context 
     * @example
     * var relMap = {}; 
     * getRel.call(relMap, node);
     * 
     * @param {Object} node
     */
    var getRel = function (node) {
        var childNodes = node.childNodes,
            child,
            rel;
        
        for(var i=0, l=childNodes.length; i<l; i++){
            child = childNodes[i];
            if(child.nodeName !== '#text' && child.nodeName !== '#comment'){
                rel = child.getAttribute('rel');
                if(rel){
                    this[rel] = child;
                }
                if (child.hasChildNodes()) {
                    getRel.call(this, child);
                }
            }        
        }
    };
    
    jsMotifClass.prototype = {

        /**
         * This method renders the HTML template, using the data and the target element 
         * specified by the caller. 
         * @param {Object} jsonObject An object containing the data to be rendered
         * @param {Object} targetElem An optional target element in which to render the data; it 
         * is specified as {String) containing the DOM id or a node reference
		 * @param {Boolean} append An flag which tells if the template should append rendered element to target Node
		 * without clearing the node content
         * @publish
         */
        render: function (jsonObject, targetElem /*?*/, append /*?*/) {
            this.jsonObject = jsonObject;
            var targetNode = (targetElem)? _dom.elem(targetElem) : this.targetElem;
            if (targetNode) {
				if(!append){
					targetNode.innerHTML = '';
                }
				fillNode.call(this, targetNode , jsonObject, this.fragment);
                this.targetElem = targetNode;
            }else{
                this.errorLog($E.LVL.critical, $E.MSG.noTargetNode, this);
            }
            
        },


        /**
         * This method renders the received data using the HTML template and returns the 
         * result as an HTML string. No events are attached.
         * @param {Object} jsonObject  An object containing the data to be rendered
         * @reutrn An HTML string into which the received data have been rendered 
         * @publish
         */
        fetch: function (jsonObject) {
            this.jsonObject = jsonObject;
            
            var tempElement = _dom.create('div',{});
            fillNode.call(this, tempElement, jsonObject, this.fragment);

            return tempElement.innerHTML;
        },

        /**
         * This method registers a handler for an attribute in a template DOM node.
         * @param {String} name The name of the attribute as a string
         * @param {Function} handler A caller-defined attribute handler function 
         * @publish
         */
        registerAttr: function(name, handler){
            this.attributes.push({name: name, handler: handler});
        },
        
        /**
         * This method adds an event handler to a template element.
         * @param {Object} rel The value of the <code>rel</code> attribute in the template node
         * @param {Object} name The name of the event, for example 'click', 'mouseout', etc.
         * @param {Object} handler An event handler function defined by the caller
         * @publish
         */
        addEvent: function(rel, name, handler) {
            if(rel && name && handler){
                if(!this.eventMap[rel]){
                    this.eventMap[rel] = [];
                }
                
                this.eventMap[rel].push({
                    name: name, 
                    handler: handler
                });    
            }
        },
        
        /**
         * This method adds an event map to the template.
         * @param {Object} events An array object assigning events and event handlers to a template node 
         * (see also the parameter <code>event</code> in the class contructor)
         * @publish
         */
        addEventMap: function(events){
            if(events && (events instanceof Array)){
                for(var i=0, l=events.length; i<l; i++){
                    var event = events[i];
                    this.addEvent(event.rel, event.name, event.handler);    
                }    
            }
                
        },
        
        /**
         * This method obtains a map object relating the <code>rel</code> attributes and the
         * DOM elements to which they apply within the caller-supplied DOM node.
         * 
         * @param {Object} node The DOM element in the template; all children of this element 
         * are checked for the <code>rel</code> attribute
         * @return An {Object} mapping the <code>rel</code> attributes and DOM elements
         * @publish
         */
        getRelMap: function (node) {
            var relMap = {};
            getRel.call(relMap, node);        
            return relMap;
        },

        /**
         * Renders single node
         * @param {Object} node
         * @param {Object} jsonObject - data needed to render node
         */
        renderObject: function (node, jsonObject) {
            var childNodes = node.childNodes,
                i=0,
                l=0,
                value,
                attribute,
                attributeName,
                processChildren;

            if (node.getAttribute) {
                for (i = 0, l = this.attributes.length; i < l; i++) {
                    attribute = this.attributes[i];
                    attributeName = attribute.name;
                    value = node.getAttribute(attributeName);
                    if(value){
                        /**
                         * If handler will return false, we shouldn't process children, 
                         * one false should stop all inner processing!
                         */
                        var handlerResult = attribute.handler.call(this, node, value, jsonObject);
                        if(processChildren !== false){
                            processChildren = handlerResult;
                        }
                        
                        
                        if (attributeName !== 'rel' && attributeName !== 'tpl' && attributeName !== 'src' && attributeName !== 'href') {
                            node.removeAttribute(attributeName);
                        }
                    }
                }
            }
           
            if(false === processChildren){
                return false;
            }

            for(i=0, l=childNodes.length; i<l; i++){
                var child = childNodes[i];
                if (child.nodeName !== '#text' && child.nodeName !== '#comment') {
                    this.renderObject(child, jsonObject);
                }
            }
        },
        
        /**
         * Function logs any errors related to wrong usage
         * @param {Object} level
         * @param {Object} params
         */
        errorLog: function(level, params/*, ...*/){
            var args = Array.prototype.slice.call(arguments, 1);
            
            if(this.debug >= level && 'object' === typeof(window.console)){
                if(level === $E.LVL.log){
                    return window.console.log.apply(window.console, args);
                }
                if(level === $E.LVL.warn){
                    return window.console.warn.apply(window.console, args);
                }
                if(level === $E.LVL.critical){
                    return window.console.error.apply(window.console, args);
                }
            }
        }


    };
    
    return jsMotifClass;
    
}());
